package studio.baka.satoripixeldungeon.levels;

import studio.baka.satoripixeldungeon.Bones;
import studio.baka.satoripixeldungeon.Dungeon;
import studio.baka.satoripixeldungeon.actors.Actor;
import studio.baka.satoripixeldungeon.actors.mobs.Mob;
import studio.baka.satoripixeldungeon.items.Generator;
import studio.baka.satoripixeldungeon.items.Heap;
import studio.baka.satoripixeldungeon.items.Item;
import studio.baka.satoripixeldungeon.items.artifacts.Artifact;
import studio.baka.satoripixeldungeon.items.journal.GuidePage;
import studio.baka.satoripixeldungeon.items.keys.GoldenKey;
import studio.baka.satoripixeldungeon.journal.Document;
import studio.baka.satoripixeldungeon.levels.builders.Builder;
import studio.baka.satoripixeldungeon.levels.builders.LoopBuilder;
import studio.baka.satoripixeldungeon.levels.painters.Painter;
import studio.baka.satoripixeldungeon.levels.rooms.Room;
import studio.baka.satoripixeldungeon.levels.rooms.secret.SecretRoom;
import studio.baka.satoripixeldungeon.levels.rooms.special.PitRoom;
import studio.baka.satoripixeldungeon.levels.rooms.special.ShopRoom;
import studio.baka.satoripixeldungeon.levels.rooms.special.SpecialRoom;
import studio.baka.satoripixeldungeon.levels.rooms.standard.EntranceRoom;
import studio.baka.satoripixeldungeon.levels.rooms.standard.ExitRoom;
import studio.baka.satoripixeldungeon.levels.rooms.standard.StandardRoom;
import studio.baka.satoripixeldungeon.levels.traps.*;
import com.watabou.utils.Bundle;
import com.watabou.utils.Random;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public abstract class RegularLevel extends Level {

    protected ArrayList<Room> rooms;

    protected Builder builder;

    protected Room roomEntrance;
    protected Room roomExit;

    public int secretDoors;

    @Override
    protected boolean build() {

        builder = builder();

        ArrayList<Room> initRooms = initRooms();
        Random.shuffle(initRooms);

        do {
            for (Room r : initRooms) {
                r.neigbours.clear();
                r.connected.clear();
            }
            //noinspection unchecked
            rooms = builder.build((ArrayList<Room>) initRooms.clone());
        } while (rooms == null);

        return painter().paint(this, rooms);

    }

    protected ArrayList<Room> initRooms() {
        ArrayList<Room> initRooms = new ArrayList<>();
        initRooms.add(roomEntrance = new EntranceRoom());
        initRooms.add(roomExit = new ExitRoom());

        int standards = standardRooms();
        for (int i = 0; i < standards; i++) {
            StandardRoom s;
            do {
                s = StandardRoom.createRoom();
            } while (!s.setSizeCat(standards - i));
            i += s.sizeCat.roomValue - 1;
            initRooms.add(s);
        }

        if (Dungeon.shopOnLevel())
            initRooms.add(new ShopRoom());

        int specials = specialRooms();
        SpecialRoom.initForFloor();
        for (int i = 0; i < specials; i++) {
            SpecialRoom s = SpecialRoom.createRoom();
            if (s instanceof PitRoom) specials++;
            initRooms.add(s);
        }

        int secrets = SecretRoom.secretsForFloor(Dungeon.depth);
        for (int i = 0; i < secrets; i++)
            initRooms.add(SecretRoom.createRoom());

        return initRooms;
    }

    protected int standardRooms() {
        return 0;
    }

    protected int specialRooms() {
        return 0;
    }

    protected Builder builder() {
        return new LoopBuilder()
                .setLoopShape(2,
                        Random.Float(0.4f, 0.7f),
                        Random.Float(0f, 0.5f));
    }

    protected abstract Painter painter();

    protected int nTraps() {
        return Random.NormalIntRange(1, 3 + (Dungeon.depth / 3));
    }

    protected Class<?>[] trapClasses() {
        return new Class<?>[]{WornDartTrap.class};
    }

    protected float[] trapChances() {
        return new float[]{1};
    }

    @Override
    public int nMobs() {
        if (Dungeon.depth == 1) {//mobs are not randomly spawned on floor 1.
            return 0;
        }
        return 2 + Dungeon.depth % 5 + Random.Int(5);
    }

    @Override
    protected void createMobs() {
        //on floor 1, 8 pre-set mobs are created so the player can get level 2.
        int mobsToSpawn = Dungeon.depth == 1 ? 8 : nMobs();

        ArrayList<Room> stdRooms = new ArrayList<>();
        for (Room room : rooms) {
            if (room instanceof StandardRoom && room != roomEntrance) {
                for (int i = 0; i < ((StandardRoom) room).sizeCat.roomValue; i++) {
                    stdRooms.add(room);
                }
            }
        }
        Random.shuffle(stdRooms);
        Iterator<Room> stdRoomIter = stdRooms.iterator();

        while (mobsToSpawn > 0) {
            Mob mob = createMob();
            Room roomToSpawn;

            if (!stdRoomIter.hasNext()) {
                stdRoomIter = stdRooms.iterator();
            }
            roomToSpawn = stdRoomIter.next();

            do {
                mob.pos = pointToCell(roomToSpawn.random());
            } while (findMob(mob.pos) != null || !passable[mob.pos] || mob.pos == exit);

            mobsToSpawn--;
            mobs.add(mob);

            if (mobsToSpawn > 0 && Random.Int(4) == 0) {
                mob = createMob();

                do {
                    mob.pos = pointToCell(roomToSpawn.random());
                } while (findMob(mob.pos) != null || !passable[mob.pos] || mob.pos == exit);

                mobsToSpawn--;
                mobs.add(mob);
            }
        }

        for (Mob m : mobs) {
            if (map[m.pos] == Terrain.HIGH_GRASS || map[m.pos] == Terrain.FURROWED_GRASS) {
                map[m.pos] = Terrain.GRASS;
                losBlocking[m.pos] = false;
            }

        }

    }

    @Override
    public int randomRespawnCell() {
        int count = 0;
        int cell;

        while (true) {

            if (++count > 30) {
                return -1;
            }

            Room room = randomRoom();
            if (room == null || room == roomEntrance) {
                continue;
            }

            cell = pointToCell(room.random(1));
            if (!heroFOV[cell]
                    && Actor.findChar(cell) == null
                    && passable[cell]
                    && room.canPlaceCharacter(cellToPoint(cell), this)
                    && cell != exit) {
                return cell;
            }

        }
    }

    @Override
    public int randomDestination() {

        int count = 0;
        int cell;

        while (true) {

            if (++count > 30) {
                return -1;
            }

            Room room = Random.element(rooms);
            if (room == null) {
                continue;
            }

            cell = pointToCell(room.random());
            if (passable[cell]) {
                return cell;
            }

        }
    }

    @Override
    protected void createItems() {

        // drops 3/4/5 items 60%/30%/10% of the time
        int nItems = 3 + Random.chances(new float[]{6, 3, 1});

        for (int i = 0; i < nItems; i++) {
            Heap.Type type;
            switch (Random.Int(20)) {
                case 0:
                    type = Heap.Type.SKELETON;
                    break;
                case 1:
                case 2:
                case 3:
                case 4:
                    type = Heap.Type.CHEST;
                    break;
                case 5:
                    type = Dungeon.depth > 1 ? Heap.Type.MIMIC : Heap.Type.CHEST;
                    break;
                default:
                    type = Heap.Type.HEAP;
            }
            int cell = randomDropCell();
            if (map[cell] == Terrain.HIGH_GRASS || map[cell] == Terrain.FURROWED_GRASS) {
                map[cell] = Terrain.GRASS;
                losBlocking[cell] = false;
            }

            Item toDrop = Generator.random();

            if (toDrop == null) continue;

            if ((toDrop instanceof Artifact && Random.Int(2) == 0) ||
                    (toDrop.isUpgradable() && Random.Int(4 - toDrop.level()) == 0)) {
                Heap dropped = drop(toDrop, cell);
                if (heaps.get(cell) == dropped) {
                    dropped.type = Heap.Type.LOCKED_CHEST;
                    addItemToSpawn(new GoldenKey(Dungeon.depth));
                }
            } else {
                Heap dropped = drop(toDrop, cell);
                dropped.type = type;
                if (type == Heap.Type.SKELETON) {
                    dropped.setHauntedIfCursed(0.75f);
                }
            }

        }

        for (Item item : itemsToSpawn) {
            int cell = randomDropCell();
            drop(item, cell).type = Heap.Type.HEAP;
            if (map[cell] == Terrain.HIGH_GRASS || map[cell] == Terrain.FURROWED_GRASS) {
                map[cell] = Terrain.GRASS;
                losBlocking[cell] = false;
            }
        }

        Item item = Bones.get();
        if (item != null) {
            int cell = randomDropCell();
            if (map[cell] == Terrain.HIGH_GRASS || map[cell] == Terrain.FURROWED_GRASS) {
                map[cell] = Terrain.GRASS;
                losBlocking[cell] = false;
            }
            drop(item, cell).setHauntedIfCursed(1f).type = Heap.Type.REMAINS;
        }

        //guide pages
        Collection<String> allPages = Document.ADVENTURERS_GUIDE.pages();
        ArrayList<String> missingPages = new ArrayList<>();
        for (String page : allPages) {
            if (!Document.ADVENTURERS_GUIDE.hasPage(page)) {
                missingPages.add(page);
            }
        }

        //these are dropped specially
        missingPages.remove(Document.GUIDE_INTRO_PAGE);
        missingPages.remove(Document.GUIDE_SEARCH_PAGE);

        int foundPages = allPages.size() - (missingPages.size() + 2);

        //chance to find a page scales with pages missing and depth
        if (missingPages.size() > 0 && Random.Float() < (Dungeon.depth / (float) (foundPages + 1))) {
            GuidePage p = new GuidePage();
            p.page(missingPages.get(0));
            int cell = randomDropCell();
            if (map[cell] == Terrain.HIGH_GRASS || map[cell] == Terrain.FURROWED_GRASS) {
                map[cell] = Terrain.GRASS;
                losBlocking[cell] = false;
            }
            drop(p, cell);
        }

    }

    public ArrayList<Room> rooms() {
        return new ArrayList<>(rooms);
    }

    //FIXME pit rooms shouldn't be problematic enough to warrant this
    public boolean hasPitRoom() {
        for (Room r : rooms) {
            if (r instanceof PitRoom) {
                return true;
            }
        }
        return false;
    }

    protected Room randomRoom() {
        Random.shuffle(rooms);
        for (Room r : rooms) {
            if (r instanceof StandardRoom) {
                return r;
            }
        }
        return null;
    }

    public Room room(int pos) {
        for (Room room : rooms) {
            if (room.inside(cellToPoint(pos))) {
                return room;
            }
        }

        return null;
    }

    protected int randomDropCell() {
        while (true) {
            Room room = randomRoom();
            if (room != null && room != roomEntrance) {
                int pos = pointToCell(room.random());
                if (passable[pos]
                        && pos != exit
                        && heaps.get(pos) == null) {

                    Trap t = traps.get(pos);

                    //items cannot spawn on traps which destroy items
                    if (!(t instanceof BurningTrap || t instanceof BlazingTrap
                            || t instanceof ChillingTrap || t instanceof FrostTrap
                            || t instanceof ExplosiveTrap || t instanceof DisintegrationTrap)) {

                        return pos;
                    }
                }
            }
        }
    }

    @Override
    public int fallCell(boolean fallIntoPit) {
        if (fallIntoPit) {
            for (Room room : rooms) {
                if (room instanceof PitRoom) {
                    int result;
                    do {
                        result = pointToCell(room.random());
                    } while (traps.get(result) != null
                            || findMob(result) != null
                            || heaps.get(result) != null);
                    return result;
                }
            }
        }

        return super.fallCell(false);
    }

    @Override
    public void storeInBundle(Bundle bundle) {
        super.storeInBundle(bundle);
        bundle.put("rooms", rooms);
    }

    @SuppressWarnings("unchecked")
    @Override
    public void restoreFromBundle(Bundle bundle) {
        super.restoreFromBundle(bundle);

        rooms = new ArrayList<>((Collection<Room>) ((Collection<?>) bundle.getCollection("rooms")));
        for (Room r : rooms) {
            r.onLevelLoad(this);
            if (r instanceof EntranceRoom) {
                roomEntrance = r;
            } else if (r instanceof ExitRoom) {
                roomExit = r;
            }
        }
    }

}
